{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Class Decorators"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's come back to decorators.\n",
    "\n",
    "So far, we have been using decorators to decorate functions - but of course, we could also use them to decorate classes:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's start with a simple example first, like we saw in the lecture:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "def savings(cls):\n",
    "    cls.account_type = 'savings'\n",
    "    return cls\n",
    "    \n",
    "def checking(cls):\n",
    "    cls.account_type = 'checking'\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Account:\n",
    "    pass\n",
    "\n",
    "@savings\n",
    "class Bank1Savings(Account):\n",
    "    pass\n",
    "\n",
    "@savings\n",
    "class Bank2Savings(Account):\n",
    "    pass\n",
    "\n",
    "@checking\n",
    "class Bank1Checking(Account):\n",
    "    pass\n",
    "\n",
    "@checking\n",
    "class Bank2Checking(Account):\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And if we inspect our classes, we'll see that the `account_type` attribute has been injected by the decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('savings', 'checking')"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Bank1Savings.account_type, Bank1Checking.account_type"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course, we could make even this simple example a little DRYer, by making a parameterized decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "def account_type(type_):\n",
    "    def decorator(cls):\n",
    "        cls.account_type = type_\n",
    "        return cls\n",
    "    return decorator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "@account_type('Savings')\n",
    "class Bank1Savings:\n",
    "    pass\n",
    "\n",
    "@account_type('Checking')\n",
    "class Bank1Checking:\n",
    "    pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "('Savings', 'Checking')"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Bank1Savings.account_type, Bank1Checking.account_type"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We're not restricted to just adding data attributes either."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's create a class decorator to inject a new function into the class before we return it:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "def hello(cls):\n",
    "    cls.hello = lambda self: f'{self} says hello!'\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "@hello\n",
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self.name = name\n",
    "        \n",
    "    def __str__(self):\n",
    "        return self.name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "mappingproxy({'__module__': '__main__',\n",
       "              '__init__': <function __main__.Person.__init__(self, name)>,\n",
       "              '__str__': <function __main__.Person.__str__(self)>,\n",
       "              '__dict__': <attribute '__dict__' of 'Person' objects>,\n",
       "              '__weakref__': <attribute '__weakref__' of 'Person' objects>,\n",
       "              '__doc__': None,\n",
       "              'hello': <function __main__.hello.<locals>.<lambda>(self)>})"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vars(Person)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see, the `Person` class now has an attribute `hello` which is a function."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, it will then become a bound method when we call it from an instance of `Person`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "p = Person('Guido')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'Guido says hello!'"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.hello()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "These examples are simple enough to understand what's going on, but not very useful."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But we can do some interesting things."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "For example, suppose we want to log every call to every callable in some class.\n",
    "\n",
    "We could certainly do it this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import wraps\n",
    "\n",
    "def func_logger(fn):\n",
    "    @wraps(fn)\n",
    "    def inner(*args, **kwargs):\n",
    "        result = fn(*args, **kwargs)\n",
    "        print(f'log: {fn.__qualname__}({args}, {kwargs}) = {result}')\n",
    "        return result\n",
    "    return inner    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @func_logger\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "    \n",
    "    @func_logger\n",
    "    def greet(self):\n",
    "        return f'Hello, my name is {self.name} and I am {self.age}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Person object at 0x7fad8914b3d0>, 'John', 78), {}) = None\n"
     ]
    }
   ],
   "source": [
    "p = Person('John', 78)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.greet((<__main__.Person object at 0x7fad8914b3d0>,), {}) = Hello, my name is John and I am 78\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Hello, my name is John and I am 78'"
      ]
     },
     "execution_count": 15,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.greet()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But this is kind of tedious if we have many methods in our class. Not very DRY!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Instead, how about creating a class decorator that will decorate every callable in a given class with the logger decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if callable(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So now we could do this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> __init__\n",
      "decorating: <class '__main__.Person'> greet\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    def __init__(self, name, age):\n",
    "        self.name = name\n",
    "        self.age = age\n",
    "    \n",
    "    def greet(self):\n",
    "        return f'Hello, my name is {self.name} and I am {self.age}'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "mappingproxy({'__module__': '__main__',\n",
       "              '__init__': <function __main__.Person.__init__(self, name, age)>,\n",
       "              'greet': <function __main__.Person.greet(self)>,\n",
       "              '__dict__': <attribute '__dict__' of 'Person' objects>,\n",
       "              '__weakref__': <attribute '__weakref__' of 'Person' objects>,\n",
       "              '__doc__': None})"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vars(Person)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Person object at 0x7fad6820ca10>, 'John', 78), {}) = None\n"
     ]
    }
   ],
   "source": [
    "p = Person('John', 78)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.greet((<__main__.Person object at 0x7fad6820ca10>,), {}) = Hello, my name is John and I am 78\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'Hello, my name is John and I am 78'"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.greet()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now we have to be a bit careful. Although this class decorator seems to work fine, it will have issues with static and class methods!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> instance_method\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        print('static_method invoked...')\n",
    "    \n",
    "    @classmethod\n",
    "    def cls_method(cls):\n",
    "        print(f'cls_method invoked for {cls}...')\n",
    "        \n",
    "    def instance_method(self):\n",
    "        print(f'instance_method invoked for {self}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "static_method invoked...\n"
     ]
    }
   ],
   "source": [
    "Person.static_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cls_method invoked for <class '__main__.Person'>...\n"
     ]
    }
   ],
   "source": [
    "Person.cls_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "instance_method invoked for <__main__.Person object at 0x7fad8914bd50>\n",
      "log: Person.instance_method((<__main__.Person object at 0x7fad8914bd50>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "Person().instance_method()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You'll notice that in the `cls_method` and `instance_method` cases, the logger printout never showed up! In fact, we did not get the message that these methods had been decorated."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "What happened?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The problem is that static and class methods are not functions, they are actually descriptors, not callables."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<staticmethod at 0x7fad6820c910>"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.__dict__['static_method']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 27,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "callable(Person.__dict__['static_method'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, they were not decorated at all."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Which is probably a good thing, because our decorator is expecting to decorate a function, not a class!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This, by the way, is why when you decorate static or class methods using a function decorator in your classes, you should do so before you decorate it with the `@staticmethod` or `@classmethod` decorators:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @staticmethod\n",
    "    @func_logger\n",
    "    def static_method():\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.static_method((), {}) = None\n"
     ]
    }
   ],
   "source": [
    "Person.static_method()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But if you try it this way around, things aren't so happy:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @func_logger\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {},
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "'staticmethod' object is not callable",
     "output_type": "error",
     "traceback": [
      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
      "\u001b[0;32m<ipython-input-31-1bb40268c11c>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mPerson\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstatic_method\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[0;32m<ipython-input-12-1eae35b7e7f4>\u001b[0m in \u001b[0;36minner\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m      4\u001b[0m     \u001b[0;34m@\u001b[0m\u001b[0mwraps\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mfn\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      5\u001b[0m     \u001b[0;32mdef\u001b[0m \u001b[0minner\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 6\u001b[0;31m         \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0margs\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      7\u001b[0m         \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34mf'log: {fn.__qualname__}({args}, {kwargs}) = {result}'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      8\u001b[0m         \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
      "\u001b[0;31mTypeError\u001b[0m: 'staticmethod' object is not callable"
     ]
    }
   ],
   "source": [
    "Person.static_method()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can actually fix this problem in our class decorator if we really wanted to."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's first examine two things separately.\n",
    "\n",
    "First let's make sure we can recognize the type of a class or static method in our class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Person:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        pass\n",
    "    \n",
    "    @classmethod\n",
    "    def class_method(cls):\n",
    "        pass"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "staticmethod"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person.__dict__['static_method'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "classmethod"
      ]
     },
     "execution_count": 34,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person.__dict__['class_method'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, can we somehow get back to the original function that was wrapped by the `@staticmethod` and `@classmethod` decorators?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The answer is yes, since these are method objects - we've seen this before when we studied the relationship between functions and descriptors and how methods were created."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function __main__.Person.static_method()>"
      ]
     },
     "execution_count": 35,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.__dict__['static_method'].__func__"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function __main__.Person.class_method(cls)>"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.__dict__['class_method'].__func__"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So now, we could modify our class decorator needs to unwrap any class or static methods, decorate the original function, and then re-wrap it with the appropriate `classmethod` or `instancemethod` decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "metadata": {},
   "outputs": [],
   "source": [
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if callable(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "        elif isinstance(obj, staticmethod):\n",
    "            original_func = obj.__func__\n",
    "            print('decorating static method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = staticmethod(decorated_func)\n",
    "            print(method, type(method))\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, classmethod):\n",
    "            original_func = obj.__func__\n",
    "            print('decorating class method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = classmethod(decorated_func)\n",
    "            setattr(cls, name, method)\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating static method <function Person.static_method at 0x7fad8914a680>\n",
      "<staticmethod object at 0x7fad6822af10> <class 'staticmethod'>\n",
      "decorating class method <function Person.class_method at 0x7fad8914ab00>\n",
      "decorating: <class '__main__.Person'> instance_method\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    @staticmethod\n",
    "    def static_method(a, b):\n",
    "        print('static_method called...', a, b)\n",
    "        \n",
    "    @classmethod\n",
    "    def class_method(cls, a, b):\n",
    "        print('class_method called...', cls, a, b)\n",
    "        \n",
    "    def instance_method(self, a, b):\n",
    "        print('instance_method called...', self, a, b)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "static_method called... 10 20\n",
      "log: Person.static_method((10, 20), {}) = None\n"
     ]
    }
   ],
   "source": [
    "Person.static_method(10, 20)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "class_method called... <class '__main__.Person'> 10 20\n",
      "log: Person.class_method((<class '__main__.Person'>, 10, 20), {}) = None\n"
     ]
    }
   ],
   "source": [
    "Person.class_method(10, 20)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "instance_method called... <__main__.Person object at 0x7fad98071950> 10 20\n",
      "log: Person.instance_method((<__main__.Person object at 0x7fad98071950>, 10, 20), {}) = None\n"
     ]
    }
   ],
   "source": [
    "Person().instance_method(10, 20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Not bad... Not what about properties?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> __init__\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self._name = name\n",
    "        \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hmm, the property was not decorated..."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's see what the type of that property is (you should already know this):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 43,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "property"
      ]
     },
     "execution_count": 43,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "type(Person.__dict__['name'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 44,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 44,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "isinstance(Person.__dict__['name'], property)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And how do we get the original functions on a property?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 45,
   "metadata": {},
   "outputs": [],
   "source": [
    "prop = Person.__dict__['name']"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 46,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function __main__.Person.name(self)>"
      ]
     },
     "execution_count": 46,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "prop.fget"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 47,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(None, None)"
      ]
     },
     "execution_count": 47,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "prop.fset, prop.fdel"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hmm, so maybe we can decorate the `fget`, `fset`, and `fdel` functions of the property (if they are not `None`).\n",
    "\n",
    "We can't just replace the functions, because `fget`, `fset` and `fdel` are actually read-only properties themselves, that return the original functions. But we could create a new property based off thge original one, substituting our decorated getter, setter and deleter.\n",
    "\n",
    "Recall that the `getter()`, `setter()` and `deleter()` methods are methods that will create a copy of the original property, but substitute the `fget`, `fset` and `fdel` methods (that's how these are used as decorators)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 48,
   "metadata": {},
   "outputs": [],
   "source": [
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if callable(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "        elif isinstance(obj, staticmethod):\n",
    "            original_func = obj.__func__\n",
    "            print('decorating static method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = staticmethod(decorated_func)\n",
    "            print(method, type(method))\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, classmethod):\n",
    "            original_func = obj.__func__\n",
    "            print('decorating class method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = classmethod(decorated_func)\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, property):\n",
    "            print('decorating property', obj)\n",
    "            if obj.fget:\n",
    "                obj = obj.getter(func_logger(obj.fget))\n",
    "            if obj.fset:\n",
    "                obj = obj.setter(func_logger(obj.fset))\n",
    "            if obj.fdel:\n",
    "                obj = obj.deleter(func_logger(obj.fdel))\n",
    "            setattr(cls, name, obj)\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 49,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> __init__\n",
      "decorating property <property object at 0x7fad89142ad0>\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self._name = name\n",
    "        \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 50,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Person object at 0x7fad6821db90>, 'David'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "p = Person('David')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 51,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.name((<__main__.Person object at 0x7fad6821db90>,), {}) = David\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'David'"
      ]
     },
     "execution_count": 51,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ha!! Pretty cool..."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's make sure it works if we have setters and deleters as well:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 52,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> __init__\n",
      "decorating property <property object at 0x7fad68232bf0>\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    def __init__(self, name):\n",
    "        self._name = name\n",
    "        \n",
    "    @property\n",
    "    def name(self):\n",
    "        return self._name\n",
    "    \n",
    "    @name.setter\n",
    "    def name(self, value):\n",
    "        self._name = value\n",
    "        \n",
    "    @name.deleter\n",
    "    def name(self):\n",
    "        print('deleting name...')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 53,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.__init__((<__main__.Person object at 0x7fad6821d7d0>, 'David'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "p = Person('David')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 54,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.name((<__main__.Person object at 0x7fad6821d7d0>,), {}) = David\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "'David'"
      ]
     },
     "execution_count": 54,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "p.name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 55,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "log: Person.name((<__main__.Person object at 0x7fad6821d7d0>, 'Beazley'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "p.name = 'Beazley'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 56,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "deleting name...\n",
      "log: Person.name((<__main__.Person object at 0x7fad6821d7d0>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "del p.name"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Success!!\n",
    "\n",
    "A bit mind-bending, but nonetheless, cool stuff!"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Still, this is not perfect... :("
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can still run into trouble because not every callable is a function that can be decorated:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 57,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.Person'> Other\n",
      "decorating: <class '__main__.Person'> other\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class Person:\n",
    "    class Other:\n",
    "        def __call__(self):\n",
    "            print('called instance of Other...')\n",
    "            \n",
    "    other = Other()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, as you see it decorated both the class `Other` (since classes are callables), and it decorated `other` since we made instances of `Other` callable too.\n",
    "\n",
    "How does that work with the logger though:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 58,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function __main__.Person.Other()>"
      ]
     },
     "execution_count": 58,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.Other"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 59,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<function __main__.func_logger.<locals>.inner()>"
      ]
     },
     "execution_count": 59,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Person.other"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And that's the problem, because `Other` and `other` are callables, they have been replaced in our class by what comes out of the decorator - a function."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So maybe we can use the `inspect` module to restrict our callables further:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 60,
   "metadata": {},
   "outputs": [],
   "source": [
    "import inspect"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 61,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyClass:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        pass\n",
    "    \n",
    "    @classmethod\n",
    "    def cls_method(cls):\n",
    "        pass\n",
    "    \n",
    "    def inst_method(self):\n",
    "        pass\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        pass\n",
    "    \n",
    "    def __add__(self, other):\n",
    "        pass\n",
    "    \n",
    "    class Other:\n",
    "        def __call__(self):\n",
    "            pass\n",
    "        \n",
    "    other = Other()\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [],
   "source": [
    "keys = ('static_method', 'cls_method', 'inst_method', 'name', '__add__', 'Other', 'other')\n",
    "inspect_funcs = ('isroutine', 'ismethod', 'isfunction', 'isbuiltin', 'ismethoddescriptor')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "('static_method', 'cls_method', 'inst_method', 'name', '__add__', 'Other', 'other')\n"
     ]
    }
   ],
   "source": [
    "print(keys)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                   static_method\tcls_method   \tinst_method  \tname         \t__add__      \tOther        \tother        \n",
      "isroutine          True         \tTrue         \tTrue         \tFalse        \tTrue         \tFalse        \tFalse        \n",
      "ismethod           False        \tFalse        \tFalse        \tFalse        \tFalse        \tFalse        \tFalse        \n",
      "isfunction         False        \tFalse        \tTrue         \tFalse        \tTrue         \tFalse        \tFalse        \n",
      "isbuiltin          False        \tFalse        \tFalse        \tFalse        \tFalse        \tFalse        \tFalse        \n",
      "ismethoddescriptor True         \tTrue         \tFalse        \tFalse        \tFalse        \tFalse        \tFalse        \n"
     ]
    }
   ],
   "source": [
    "max_header_length = max(len(key) for key in keys)\n",
    "max_fname_length = max(len(func) for func in inspect_funcs)\n",
    "print(format('', f'{max_fname_length}s'), '\\t'.join(format(key, f'{max_header_length}s') for key in keys))\n",
    "for inspect_func in inspect_funcs:\n",
    "    fn = getattr(inspect, inspect_func)\n",
    "    inspect_results = (format(str(fn(MyClass.__dict__[key])), f'{max_header_length}s') for key in keys)\n",
    "    print(format(inspect_func, f'{max_fname_length}s'), '\\t'.join(inspect_results))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As you can see we could use inspect to only pick things that are routines instead of more general callables. Properties, static and class methods we are already handling specially, so I'm going to move the callable check last in the `if...elif` block so we handle static and class methods first (since they are classified as routines too)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {},
   "outputs": [],
   "source": [
    "import inspect\n",
    "\n",
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if isinstance(obj, staticmethod):\n",
    "            original_func = obj.__func__\n",
    "            print('decorating static method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = staticmethod(decorated_func)\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, classmethod):\n",
    "            original_func = obj.__func__\n",
    "            print('decorating class method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = classmethod(decorated_func)\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, property):\n",
    "            print('decorating property', obj)\n",
    "            if obj.fget:\n",
    "                obj = obj.getter(func_logger(obj.fget))\n",
    "            if obj.fset:\n",
    "                obj = obj.setter(func_logger(obj.fset))\n",
    "            if obj.fdel:\n",
    "                obj = obj.deleter(func_logger(obj.fdel))\n",
    "            setattr(cls, name, obj)\n",
    "        elif inspect.isroutine(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating static method <function MyClass.static_method at 0x7fad6821c0e0>\n",
      "decorating class method <function MyClass.cls_method at 0x7fad6821c320>\n",
      "decorating: <class '__main__.MyClass'> inst_method\n",
      "decorating property <property object at 0x7fad6820f5f0>\n",
      "decorating: <class '__main__.MyClass'> __add__\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class MyClass:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        print('static_method called...')\n",
    "    \n",
    "    @classmethod\n",
    "    def cls_method(cls):\n",
    "        print('class method called...')\n",
    "    \n",
    "    def inst_method(self):\n",
    "        print('instance method called...')\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        print('name getter called...')\n",
    "    \n",
    "    def __add__(self, other):\n",
    "        print('__add__ called...')\n",
    "    \n",
    "    class Other:\n",
    "        def __call__(self):\n",
    "            print(f'{self}.__call__ called...')\n",
    "        \n",
    "    other = Other()\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(__main__.MyClass.Other, <__main__.MyClass.Other at 0x7fad6828bb10>)"
      ]
     },
     "execution_count": 67,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "MyClass.Other, MyClass.other"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<__main__.MyClass.Other object at 0x7fad6828bb10>.__call__ called...\n"
     ]
    }
   ],
   "source": [
    "MyClass.other()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No log, that was expected."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "static_method called...\n",
      "log: MyClass.static_method((), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass.static_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "class method called...\n",
      "log: MyClass.cls_method((<class '__main__.MyClass'>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass.cls_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "instance method called...\n",
      "log: MyClass.inst_method((<__main__.MyClass object at 0x7fad682c19d0>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass().inst_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "name getter called...\n",
      "log: MyClass.name((<__main__.MyClass object at 0x7fad6828b650>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass().name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "__add__ called...\n",
      "log: MyClass.__add__((<__main__.MyClass object at 0x7fad8914bb50>, <__main__.MyClass object at 0x7fad8914b5d0>), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass() + MyClass()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "If we really wanted to, we could also decorate the `Other` class:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.MyClass.Other'> __call__\n",
      "decorating static method <function MyClass.static_method at 0x7fad68239b90>\n",
      "decorating class method <function MyClass.cls_method at 0x7fad68239170>\n",
      "decorating: <class '__main__.MyClass'> inst_method\n",
      "decorating property <property object at 0x7fad682185f0>\n",
      "decorating: <class '__main__.MyClass'> __add__\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class MyClass:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        print('static_method called...')\n",
    "    \n",
    "    @classmethod\n",
    "    def cls_method(cls):\n",
    "        print('class method called...')\n",
    "    \n",
    "    def inst_method(self):\n",
    "        print('instance method called...')\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        print('name getter called...')\n",
    "    \n",
    "    def __add__(self, other):\n",
    "        print('__add__ called...')\n",
    "    \n",
    "    @class_logger\n",
    "    class Other:\n",
    "        def __call__(self):\n",
    "            print(f'{self}.__call__ called...')\n",
    "        \n",
    "    other = Other()\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "<__main__.MyClass.Other object at 0x7fad8912f3d0>.__call__ called...\n",
      "log: MyClass.Other.__call__((<__main__.MyClass.Other object at 0x7fad8912f3d0>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass.other()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could also do a bit of DRYing on our decorator code.\n",
    "\n",
    "First let's handle the static and class methods:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "metadata": {},
   "outputs": [],
   "source": [
    "import inspect\n",
    "\n",
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if isinstance(obj, staticmethod) or isinstance(obj, classmethod):\n",
    "            type_ = type(obj)\n",
    "            original_func = obj.__func__\n",
    "            print(f'decorating {type_.__name__} method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = type_(decorated_func)\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, property):\n",
    "            print('decorating property', obj)\n",
    "            if obj.fget:\n",
    "                obj = obj.getter(func_logger(obj.fget))\n",
    "            if obj.fset:\n",
    "                obj = obj.setter(func_logger(obj.fset))\n",
    "            if obj.fdel:\n",
    "                obj = obj.deleter(func_logger(obj.fdel))\n",
    "            setattr(cls, name, obj)\n",
    "        elif inspect.isroutine(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.MyClass.Other'> __call__\n",
      "decorating staticmethod method <function MyClass.static_method at 0x7fad6820bb00>\n",
      "decorating classmethod method <function MyClass.cls_method at 0x7fad6820b0e0>\n",
      "decorating: <class '__main__.MyClass'> inst_method\n",
      "decorating property <property object at 0x7fad68218e30>\n",
      "decorating: <class '__main__.MyClass'> __add__\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class MyClass:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        print('static_method called...')\n",
    "    \n",
    "    @classmethod\n",
    "    def cls_method(cls):\n",
    "        print('class method called...')\n",
    "    \n",
    "    def inst_method(self):\n",
    "        print('instance method called...')\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        print('name getter called...')\n",
    "    \n",
    "    def __add__(self, other):\n",
    "        print('__add__ called...')\n",
    "    \n",
    "    @class_logger\n",
    "    class Other:\n",
    "        def __call__(self):\n",
    "            print(f'{self}.__call__ called...')\n",
    "        \n",
    "    other = Other()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "static_method called...\n",
      "log: MyClass.static_method((), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass.static_method()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "class method called...\n",
      "log: MyClass.cls_method((<class '__main__.MyClass'>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass.cls_method()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, let's see if we can clean up the block to handle properties - I don't like these repeated nested if statements that basically do the almost same thing:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 80,
   "metadata": {},
   "outputs": [],
   "source": [
    "import inspect\n",
    "\n",
    "def class_logger(cls):\n",
    "    for name, obj in vars(cls).items():\n",
    "        if isinstance(obj, staticmethod) or isinstance(obj, classmethod):\n",
    "            type_ = type(obj)\n",
    "            original_func = obj.__func__\n",
    "            print(f'decorating {type_.__name__} method', original_func)\n",
    "            decorated_func = func_logger(original_func)\n",
    "            method = type_(decorated_func)\n",
    "            setattr(cls, name, method)\n",
    "        elif isinstance(obj, property):\n",
    "            print('decorating property', obj)\n",
    "            methods = (('fget', 'getter'), ('fset', 'setter'), ('fdel', 'deleter'))\n",
    "            for prop, method in methods:\n",
    "                if getattr(obj, prop):\n",
    "                    obj = getattr(obj, method)(func_logger(getattr(obj, prop)))\n",
    "            setattr(cls, name, obj)\n",
    "        elif inspect.isroutine(obj):\n",
    "            print('decorating:', cls, name)\n",
    "            setattr(cls, name, func_logger(obj))\n",
    "    return cls"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 81,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "decorating: <class '__main__.MyClass.Other'> __call__\n",
      "decorating staticmethod method <function MyClass.static_method at 0x7fad8914a170>\n",
      "decorating classmethod method <function MyClass.cls_method at 0x7fad8914a3b0>\n",
      "decorating: <class '__main__.MyClass'> inst_method\n",
      "decorating property <property object at 0x7fad682be950>\n",
      "decorating: <class '__main__.MyClass'> __add__\n"
     ]
    }
   ],
   "source": [
    "@class_logger\n",
    "class MyClass:\n",
    "    @staticmethod\n",
    "    def static_method():\n",
    "        print('static_method called...')\n",
    "    \n",
    "    @classmethod\n",
    "    def cls_method(cls):\n",
    "        print('class method called...')\n",
    "    \n",
    "    def inst_method(self):\n",
    "        print('instance method called...')\n",
    "    \n",
    "    @property\n",
    "    def name(self):\n",
    "        print('name getter called...')\n",
    "        \n",
    "    @name.setter\n",
    "    def name(self, value):\n",
    "        print('name setter called...')\n",
    "        \n",
    "    @name.deleter\n",
    "    def name(self):\n",
    "        print('name deleter called...')\n",
    "    \n",
    "    def __add__(self, other):\n",
    "        print('__add__ called...')\n",
    "    \n",
    "    @class_logger\n",
    "    class Other:\n",
    "        def __call__(self):\n",
    "            print(f'{self}.__call__ called...')\n",
    "        \n",
    "    other = Other()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 82,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "name getter called...\n",
      "log: MyClass.name((<__main__.MyClass object at 0x7fad98076fd0>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass().name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 83,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "name setter called...\n",
      "log: MyClass.name((<__main__.MyClass object at 0x7fad980764d0>, 'David'), {}) = None\n"
     ]
    }
   ],
   "source": [
    "MyClass().name = 'David'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 84,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "name deleter called...\n",
      "log: MyClass.name((<__main__.MyClass object at 0x7fad98076410>,), {}) = None\n"
     ]
    }
   ],
   "source": [
    "del MyClass().name"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
